module VirtualCity.CityDescription

open Casanova
open Casanova.Utilities
open Casanova.Core
open Casanova.Coroutines
open Casanova.Game
open Casanova.Input
open Casanova.Math
open Casanova.Drawing
open System


/// A bus has a unique identifier, its number,
/// and a price for boarding it.
type [<CasanovaEntity>] BusDescription = {
  MaxSpeed    : float32<m/s>
  TicketPrice : Money
  Number      : int
}


/// A bus stop has a unique identifier, its name,
/// serves a bus with a given number, is at a specified
/// location in the city, and the bus stops there at a
/// given time. Different stops may share the same
/// location in order to serve multiple buses.
and [<CasanovaEntity>] BusStopDescription = {
  Name           : string
  BusNumber      : int
  Location       : Vector2<m>
  Time           : DateTime
}


/// A building simply has a position in the city.
and [<CasanovaEntity>]  BuildingDescription = {
  Position : Vector2<m>
}


/// A road has a name, and two end-points it connects.
and [<CasanovaEntity>] RoadDescription = { 
  Name     : string
  Start    : Vector2<m>
  End      : Vector2<m>
}


/// Stats represent the current well-being of an agent; they are:
/// - how rested the agent is (sleep);
/// - how relaxed the agent is (stress);
/// - how sated the agent is (hunger);
/// - how fit the agent is (exercise);
/// - how enjoyed the agent is (fun).
and [<CasanovaEntity>] Stats<[<Measure>]'u> = {
  Rested  : float32<'u>
  Relaxed : float32<'u>
  Sated   : float32<'u>
  Fit     : float32<'u>
  Enjoyed : float32<'u>
} with
  /// Initialize a set of stats all to zero
  static member Zero : Stats<'u> = 
    {
      Rested  = 0.0f<_>
      Relaxed = 0.0f<_>
      Sated   = 0.0f<_>
      Fit     = 0.0f<_>
      Enjoyed = 0.0f<_>
    }        
  /// Initialize a set of stats all to zero
  static member One : Stats<'u> = 
    {
      Rested  = 1.0f |> with_measure
      Relaxed = 1.0f |> with_measure
      Sated   = 1.0f |> with_measure
      Fit     = 1.0f |> with_measure
      Enjoyed = 1.0f |> with_measure
    }        
  /// Initialize a set of stats all to zero apart from Rested,
  /// which is set to one.
  static member OneRested : Stats<'u> = 
    {
      Stats<'u>.Zero with Rested  = 1.0f |> with_measure
    }        
  /// Initialize a set of stats all to zero apart from Relaxed,
  /// which is set to one.
  static member OneRelaxed : Stats<'u> = 
    {
      Stats<'u>.Zero with Relaxed = 1.0f |> with_measure
    }        
  /// Initialize a set of stats all to zero apart from Sated,
  /// which is set to one.
  static member OneSated : Stats<'u> = 
    {
      Stats<'u>.Zero with Sated = 1.0f |> with_measure
    }        
  /// Initialize a set of stats all to zero apart from Fit,
  /// which is set to one.
  static member OneFit : Stats<'u> = 
    {
      Stats<'u>.Zero with Fit = 1.0f |> with_measure
    }        
  /// Initialize a set of stats all to zero apart from Enjoyed,
  /// which is set to one.
  static member OneEnjoyed : Stats<'u> = 
    {
      Stats<'u>.Zero with Enjoyed = 1.0f |> with_measure
    }        
  static member private map(s:Stats<'u>,f:float32<'u> -> float32<'v>) =
    {
      Rested  = s.Rested  |> f
      Relaxed = s.Relaxed |> f
      Sated   = s.Sated   |> f
      Fit     = s.Fit     |> f
      Enjoyed = s.Enjoyed |> f
    }    
  /// Multiply all the stats by a floating point number.
  static member (*)(s:Stats<'u>,x:float32<'v>) = Stats.map(s,fun v -> x * v)
  static member private zip(s:Stats<'u>,s':Stats<'v>,f:float32<'u> -> float32<'v> -> float32<'w>) =
    {
      Rested  = s'.Rested  |> f s.Rested
      Relaxed = s'.Relaxed |> f s.Relaxed
      Sated   = s'.Sated   |> f s.Sated
      Fit     = s'.Fit     |> f s.Fit
      Enjoyed = s'.Enjoyed |> f s.Enjoyed
    }
  /// Add two sets of stats together.
  static member (+)(s:Stats<'u>,s':Stats<'u>) = Stats.zip(s,s',(fun x y -> x + y |> min (1.0f |> with_measure)))
  /// Multiply two sets of stats component by component.
  static member Multiply(s:Stats<'u>,s':Stats<'u>) = Stats.zip(s,s',(fun x y -> x * y))
  /// Subtract two sets of stats.
  static member (-)(s:Stats<'u>,s':Stats<'u>) = Stats.zip(s,s',(fun x y -> x - y |> max 0.0f<_>))
  /// Compare two sets of stats.
  static member LessEqual(s:Stats<'u>,s':Stats<'u>) =
      s.Rested  <= s'.Rested &&
      s.Relaxed <= s'.Relaxed &&
      s.Sated   <= s'.Sated &&
      s.Fit     <= s'.Fit &&
      s.Enjoyed <= s'.Enjoyed
  static member private map_reduce(s:Stats<'u>,f,g) =
    [ s.Rested |> f
      s.Relaxed |> f
      s.Sated |> f
      s.Fit |> f
      s.Enjoyed |> f ] |> List.reduce g
  static member GreaterThan(s:Stats<'u>,threshold) =
    Stats.map_reduce (s, (fun x -> x > threshold), (&&))
  /// Perform the log-sum of all the elements. This is 
  /// roughly equivalent to multiplying all elements, but
  /// without the associated loss of precision.
  member s.LogSum = Stats.map_reduce(s, (fun x -> log (x |> float32)), (+))
  member s.Min = Stats.map_reduce(s, id, min)
  /// Get the minimum stat between the vital ones (sleep and hunger).
  member s.VitalMin = 
    [ s.Rested
      s.Sated ] |> List.min
  /// Get the minimum stat between the non-vital ones.
  member s.NonVitalMin = 
    [ s.Relaxed 
      s.Fit 
      s.Enjoyed ] |> List.min
  /// Get the current stress handicap given the non vital minimum stat.
  member s.StressHandicap =
    let h = s.NonVitalMin |> float32
    let h = 1.0f - h
    h * 0.2f + 1.0f
  /// Get the maximum stat
  member s.Max = Stats.map_reduce(s, id, max)
  override s.ToString() =
    sprintf "{\n  Rested  = %1.2f\n  Relaxed = %1.2f\n  Sated   = %1.2f\n  Fit     = %1.2f\n  Enjoyed = %1.2f\n }" 
            (s.Rested |> float32) (s.Relaxed |> float32) (s.Sated |> float32) (s.Fit |> float32) (s.Enjoyed |> float32)
  member s.ToString2() =
    sprintf "{\n  Rested  = %1.2f\n  Relaxed = %1.2f\n  Hunger  = %1.2f\n  Fit     = %1.2f\n  Enjoyed = %1.2f\n }" 
            (s.Rested |> float32) (s.Relaxed |> float32) (s.Sated |> float32) (s.Fit |> float32) (s.Enjoyed |> float32)


/// An agent has a position, a set of stats, a set of items, and owns some money.
/// An agent also has a home and a work place.
and [<CasanovaEntity>]  AgentDescription = {
  Position  : Vector2<m>
  Stats     : Stats<1>
  Money     : Money
  MaxSpeed  : float32<m/s>
  Inventory : List<ItemDescription>
  Work      : BuildingDescription
  Home      : BuildingDescription
} with 
  member agent.IsAlive = 
    Stats<1>.GreaterThan(agent.Stats, 0.0f<1>)
  static member internal IdleStatsLossBase : Stats<1/s> =
    {
      Rested  = (1.0f / 150000.0f) |> with_measure
      Relaxed = (1.0f / 150000.0f) |> with_measure
      Sated   = (1.0f / 720000.0f) |> with_measure
      Fit     = (1.0f / 720000.0f) |> with_measure
      Enjoyed = (1.0f / 150000.0f) |> with_measure
    }
  member self.IdleStatsLoss = AgentDescription.IdleStatsLossBase


/// An item has a name, may be used with certain effects, and
/// its use may be restricted to a certain set of times.
and [<CasanovaEntity>] ItemDescription = {
  Name            : string
  OpenUse         : bool
  UseEffects      : List<UseEffects>
  UseSlots        : List<UseSlot>
}


/// A use slot represents the time in which an agent may use
/// a given item; it consists of the initial time since when
/// the item becomes available, plus the amount of time for
/// which the item remains available.
and [<CasanovaEntity>] UseSlot = {
  Start           : TimeSpan
  Duration        : TimeSpan
} with 
  /// Check if a certain time is contained in a given use slot
  static member Contains (time:DateTime) (slot:UseSlot) =
    let time = TimeSpan(time.Hour, time.Minute, time.Second)
    slot.Start <= time && slot.Start + slot.Duration >= time
  /// Return a list of use slots that represents availability
  /// for a whole day.
  static member AllDay =
    [
      {
        Start           = TimeSpan.Zero
        Duration        = TimeSpan.FromHours(24.0)
      }
    ]
  /// Return a list of use slots that represents availability
  /// between two hours, for example (08:00,17:00).
  static member HourRange(from,until) =
    [
      {
        Start           = TimeSpan.FromHours(from |> float)
        Duration        = TimeSpan.FromHours((until - from) |> float)
      }
    ]


/// The effects for using an item are represented with:
/// - a description of the effects;
/// - the stats, money, and items obtained as a result of this action. Items appear at the agent home;
/// - the stats, money, and items lost as a result of this action. Items are removed from the agent home;
/// - the time spent performing this action
and [<CasanovaEntity>] UseEffects = { 
  Description     : string
  StatsGained     : Stats<1>
  MoneyGained     : Money
  ItemsGained     : List<Ref<ItemDescription>>
  StatsLost       : Stats<1>
  MoneyLost       : Money
  ItemsLost       : List<string>
  TimeSpent       : float32<s>
} with
  /// Initialize a UseEffects instance with all values set to zero
  static member Zero =
    {
      Description     = ""
      StatsGained     = Stats.Zero
      ItemsGained     = []
      StatsLost       = Stats.Zero
      MoneyGained     = 0.0f<eur>
      MoneyLost       = 0.0f<eur>
      ItemsLost       = []
      TimeSpent       = 0.0f<_>
    }
  /// Create a UseEffect instance with a description, the stats gained, the stats lost, 
  /// the items lost, the money gained, and the time spent.
  static member Create(description,stats_gained,stats_lost,items_lost,money_gained,time_spent) =
    {
      UseEffects.Zero 
        with
          Description     = description
          StatsGained     = stats_gained
          StatsLost       = stats_lost
          ItemsLost       = items_lost
          MoneyGained     = money_gained
          TimeSpent       = time_spent
    }
  /// Create a UseEffect instance with a description, the stats gained, the stats lost, 
  /// the items lost, and the time spent.
  static member Create(description,stats_gained,stats_lost,items_lost,time_spent) =
    UseEffects.Create(description,stats_gained,stats_lost,items_lost,0.0f<eur>,time_spent)
  /// Create a UseEffect instance with a description, the stats gained, the stats lost, 
  /// the money gained, and the time spent.
  static member Create(description,stats_gained,stats_lost,money_gained,time_spent) =
    UseEffects.Create(description,stats_gained,stats_lost,[],money_gained,time_spent)
  /// Create a UseEffect instance with a description, the stats gained, the stats lost, 
  /// and the time spent.
  static member Create(description,stats_gained,stats_lost,time_spent) =
    UseEffects.Create(description,stats_gained,stats_lost,[],time_spent)


and Money = float32<eur>


/// The walk-graph contains all the walkable links that agents
/// may walk over. These links represent roads and building
/// entrances.
and [<CasanovaEntity>] WalkGraph = { 
//  Nodes     : List<Vector2<m>>
  Links     : List<WalkNode * WalkNode>
}


and [<CasanovaEntity>] WalkNode = {
  Position    : Vector2<m>
  IsBuilding  : bool
}
